Self-certifying machine ids + a per-IP rate rule (fleet-trust stage 1) - #39
Merged
Conversation
Machine ids become <slug>-<tag>: the slug is the old mint verbatim (hostname slug + 4 random hex), the tag the first 8 lowercase hex of HMAC-SHA256(DAEMON_SECRET, "flue-machine-id/" + slug). Setup, join and the Remote-screen deploy all hold the secret at mint time and mint through the same config.MintMachineID; the Worker recomputes the tag statelessly before idFromName, so a forged or stale id earns the same 404 a malformed one does and no Durable Object wakes. On the daemon leg the tag is checked after the bearer secret — tag-first would hand the one unmetered route a tag oracle (404 vs 401) and a free HMAC per anonymous probe. The credential-less routes (/client/*, POST /api/pair/*) gain a Cloudflare rate-limiting binding (CLIENT_RATE, 300 per minute per IP), carried identically by relay/wrangler.jsonc for dev and by the API-driven deploy (internal/relaydeploy, a "ratelimit" metadata binding in internal/cloudflare). The Worker fails open without the binding: the rule bounds quota burn, not access. The daemon leg stays unmetered — secret-gated, one socket per machine. testdata/relay/machine-ids.json pins the tag arithmetic across languages: internal/config generates it (go test ./internal/config/ -update) and re-derives every case on ordinary runs, and the Worker suite walks the committed file. Breaking change, no back-compat: pre-tag ids are refused everywhere, and the sole deployment re-joins. Implements stage 1 of spec/fleet-trust.md (the spec rides its own branch and is deliberately not part of this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements stage 1 of the fleet-trust spec (
spec/fleet-trust.md, riding its own branch in PR #37): the "Self-certifying machine ids" and "Rate rule" sections, and nothing from the fleet-key/certificate/directory sections. The spec file itself is deliberately not in this diff.What the open id namespace cost
The Worker routes with
idFromName(id), and a stateless router cannot know which ids exist — so any grammar-valid id woke a Durable Object. Every scan, typo and probe was a billed DO wake, and the id namespace was effectively "anything lowercase under 64 characters".The MAC scheme
flue relay setup,flue relay join, and the Remote-screen deploy (all three callconfig.MintMachineID, which now takes the secret; the signature change is what proves no mint site was missed).relay/src/index.ts) verifies the tag statelessly beside the grammar check, beforeidFromName: a bad tag is the same404 {"error":"no such machine"}a malformed id gets, and no DO wakes. The HMAC only runs after the cheap regex passes (run_worker_firstputs this on every request).internal/transport/relay); the sole deployment re-joins. Ids grow nine characters (mac-a1b2-3f9a12cd), and rotating the secret invalidates every id — which re-setup's re-join re-mints anyway.The rate rule: Cloudflare's rate-limiting binding shipped
The preferred mechanism shipped, not the in-Worker fallback. Verified all three legs can carry it before choosing:
{"type":"ratelimit","name":…,"namespace_id":…,"simple":{"limit":…,"period":…}}— now emitted byinternal/cloudflare(pinned intestdata/deploy_metadata.jsonand asserted end-to-end throughflue relay setup's fake-API test) and wired ininternal/relaydeploy.ratelimitskey, same name/namespace/numbers; a comment marks the twin. wrangler 4's config schema carries it and miniflare simulates it — the vitest pool instantiates the binding, and a test assertsenv.CLIENT_RATEexists so the two paths can't drift silently.allowRatekeys onCF-Connecting-IPover/client/*andPOST /api/pair/*, answers429 {"error":"rate limited"}, and runs after the grammar check, before the tag HMAC. 300/min per IP per Cloudflare location — order-of-100/min per the spec: fleet tabs (reconnect storms included) never see it; burning quota or walking the 2^32 tag space needs a botnet. Fail-open when the binding is absent (it bounds cost, not access), and the daemon leg is exempt (secret-gated, one socket per machine).GET /api/pair/<id>(a browser following a pairing link, answered from unmetered assets) spends no limiter token.The fixture
testdata/relay/machine-ids.jsonpins slug→tag cross-language, likeframes.jsonbefore it: Go generates (go test ./internal/config/ -update) and re-derives every committed case on ordinary runs; the Worker suite (relay/test/machineid.test.ts) walks the committed file. Cases cover the hostname-fallback slug, the 24-char truncation ceiling, inner double dashes, a hex-shaped slug (the trap for a parser hunting "the hex part" instead of "the last nine characters"), and one slug tagged under two secrets. Thetest-secretcases are minted under the pool's ownDAEMON_SECRET, so one test drives a Go-minted id through the real TS router end to end.Test ids across both suites now mint through a shared helper (
relay/test/harness.ts,machineId()), which is the same HMAC the router verifies.Docs
spec/relay-protocol.md(Auth grammar + Conformance) anddocs/RELAY.md(id shape, join-line failure mode, re-setup consequences, fair-use section — the old "add a WAF rule yourself" paragraph now describes the shipped rule) updated.Web sources needed no functional change — the browser receives ids from pairing links and never constructs or verifies one; its record grammar is a deliberate superset that tagged ids already satisfy. Only a comment there was corrected (it claimed exactness with the relay grammar), which is why the web suite was run.
Test evidence
go vet ./...clean;go test ./...all packages ok (aftermake web relay)cd relay && pnpm test: 5 files, 126 passed (incl. new fixture walk, tag-routing and rate-rule suites)cd web && pnpm vitest run: 56 files, 998 passed;pnpm run lintclean🤖 Generated with Claude Code